package api;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import model.Manipulator;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;

import api.error.ConnectionUnavailableException;
import api.error.InproperProgramException;

public class Program {

	private Document doc;
	private static ProgramImplementor imp;
	private Set<Manipulator> manips;
	private static int forLoopCounter = 0;
	
	public Program(File f, ProgramImplementor imp) throws SAXException, IOException, ParserConfigurationException, InproperProgramException
	{
		this.doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(f);
		Program.imp = imp;//there can only ever be one implementation running at a time
	}
	private Set<Manipulator> getManipulators()
	{
		if(manips == null)
			manips = new TreeSet<Manipulator>();
		return manips;
	}
	public void execute() throws InproperProgramException, ConnectionUnavailableException
	{
		new Thread(){
			public void run(){
				doIt();
			}
			public void doIt()
			{
				NodeList roots = doc.getElementsByTagName("program");
				if(roots.getLength() == 0){
					imp.exception("No PROGRAM tag found");
					return;
				}
				
				NodeList configs = ((Element)roots.item(0)).getElementsByTagName("configuration");
				if(configs.getLength() == 0)
				{
					imp.exception("No CONFIGURATION tag found");
					return;
				}
			
				NodeList execs = ((Element)roots.item(0)).getElementsByTagName("execution");
				if(execs.getLength() == 0)
				{
					imp.exception("No EXECUTION tag found");
					return;
				}
				
				
				if(doConfig((Element)configs.item(0)))
				{
					doExecution((Element)execs.item(0));
					doRelease((Element)configs.item(0));
				}
			}
		}.start();
	}
	
	private boolean doConfig(Element config)
	{
		List<Element> conns = getElementChildren(config, "conn");
		for(Element conn : conns)
		{
			String var = conn.getAttribute("variable");
			String connSt = conn.getAttribute("connection");
			String interpret = conn.getAttribute("interpreter");
			Manipulator m = imp.createConnection(var, connSt, interpret);
			if(m == null)
				return false;
			getManipulators().add(m);
		}
		return true;
	}
	private void doExecution(Element exec)
	{
		String timeoutSt = exec.getAttribute("timeout");
		int defTimeout = Integer.MAX_VALUE;
		try{
			defTimeout = Integer.parseInt(timeoutSt);
		}catch(Exception e)
		{
			imp.exception("No DEFAULT TIMEOUT was given for the execution steps");
			return;
		}
		
		CommandElement elem = null;
		try{
			elem = getCommand(exec, defTimeout);
		}catch(InproperProgramException ipe)
		{
			imp.exception("IPE: " + ipe.getMessage());
			return;
		}
		elem.execute(true);
		while(!elem.quit() && !elem.finished())
		{
			try{
				Thread.sleep(10);
			}catch(Exception e){}
		}
		if(elem.quit())
		{
			imp.exception("Program Quit Unexpectedly");
		}
		
	}

	private void doRelease(Element rel)
	{
		List<Element> conns =getElementChildren(rel, "conn");
		for(Element conn : conns)
		{
			String port = conn.getAttribute("connection");
			if(port == null)
				continue;
			Iterator<Manipulator> it = getManipulators().iterator();
			while(it.hasNext())
			{
				Manipulator m = it.next();
				if(m.getIO().getName().equalsIgnoreCase(port)){
					imp.releaseConnection(m);
					break;
				}
			}
		}
	}
	private static CommandStep getStep(Element el, int defaultTimeout)
	{
		int timeout = getTimeout(el, defaultTimeout);
		String command = getText(el);
		String error = el.getAttribute("on-error");
		if(command != null && command.length() > 0)
			return new CommandStep(command, timeout, (error != null && error.equals("quit")));
		return null;

	}
	private static CommandSequence getSequence(Element el, int defaultTimeout) throws InproperProgramException
	{
		int timeout = getTimeout(el, defaultTimeout);

		List<CommandElement> csteps = new ArrayList<CommandElement>();
		for(int i = 0; i < el.getChildNodes().getLength(); i++)
		{
			Node n = el.getChildNodes().item(i);
			if(n.getNodeType() == Node.ELEMENT_NODE)
			{
				Element e = (Element)n;
				CommandElement elem = getCommand(e, timeout);
				csteps.add(elem);
			}
		}
		
		return new CommandSequence(csteps);
	}
	private static CommandSet getSet(Element el, int defaultTimeout) throws InproperProgramException
	{
		int timeout = getTimeout(el, defaultTimeout);
		List<CommandElement> csteps = new ArrayList<CommandElement>();
		for(Element e : getElementChildren(el))
		{
			CommandElement ce = getCommand(e, timeout);
			csteps.add(ce);
		}
		
		return new CommandSet(csteps);
	}
	private static CommandWait getWait(Element el)
	{
		int length = getTimeout(el, 0);
		return new CommandWait(length);
	}
	private static CommandPrompt getPrompt(Element el, int defaultTimeout) throws InproperProgramException
	{
		int timeout = getTimeout(el, defaultTimeout);
		String question = el.getAttribute("question");
		if(question == null || question.trim().length() == 0)
			throw new InproperProgramException("No question was supplied for prompt.");
		List<Element> options = getElementChildren(el, "option");
		HashMap<String, CommandElement> map = new HashMap<String, CommandElement>();
		for(Element option : options)
		{
			String value = option.getAttribute("value");
			
			List<Element> children = getElementChildren(option);
			List<CommandElement> elems = new ArrayList<CommandElement>();
			for(Element e : children)
			{
				CommandElement elem = getCommand(e, timeout);
				elems.add(elem);
			}
			
			if(value != null && value.trim().length() > 0 && elems.size() > 0)
				map.put(value, new CommandSequence(elems));
		}
		if(map.size() == 0)
		{
			map.put("Continue", null);
		}
		
		return new CommandPrompt(question==null?"":question, map);
	}
	private static CommandFor getFor(Element el, int defaultTimeout) throws InproperProgramException
	{
		int timeout = getTimeout(el, defaultTimeout);
		String countSt = el.getAttribute("count");
		int count = 1;
		if(countSt.equals("none") || countSt.equals("infinity") || countSt.equals("continue") || countSt.equals("until"))
		{
			count = Integer.MAX_VALUE;
			if(el.getElementsByTagName("break").getLength() == 0)
				throw new InproperProgramException("No break statement on infinite loop.");
		}
		else
		{
			try{count = Integer.parseInt(countSt);}catch(Exception e){}
		}
		NodeList children = el.getChildNodes();
		List<CommandElement> elems = new ArrayList<CommandElement>();
		
		for(int i = 0; i < children.getLength(); i++)
		{
			if(children.item(i).getNodeType() != Node.ELEMENT_NODE)
				continue;
			
			Element e = ((Element)children.item(i));
			CommandElement elem = getCommand(e, timeout);
			elems.add(elem);
		}
		CommandSequence seq = new CommandSequence(elems);
		return new CommandFor(count, seq);
	}
	private static CommandElement getCommand(Element el, int defaultTimeout) throws InproperProgramException
	{
		String name = el.getTagName();
		if(name.equals("step"))
			return getStep(el, defaultTimeout);
		else if(name.equals("set"))
			return getSet(el, defaultTimeout);
		else if(name.equals("sequence") || name.equals("execution"))
			return getSequence(el, defaultTimeout);
		else if(name.equals("wait"))
			return getWait(el);
		else if(name.equals("prompt"))
			return getPrompt(el, defaultTimeout);
		else if(name.equals("for"))
			return getFor(el, defaultTimeout);
		else if(name.equals("break"))
			return new CommandBreak();
		else
			throw new InproperProgramException("Could not create a command from <" + el.getNodeName() + ">");
	}
	private static List<Element> getElementChildren(Node e)
	{
		List<Element> list = new ArrayList<Element>();
		for(int i = 0; i < e.getChildNodes().getLength(); i++)
		{
			Node n = e.getChildNodes().item(i);
			if(n.getNodeType() == Node.ELEMENT_NODE)
				list.add((Element)n);
		}
		
		return list;
	}
	private static List<Element> getElementChildren(Node e, String tag)
	{
		List<Element> list = new ArrayList<Element>();
		for(int i = 0; i < e.getChildNodes().getLength(); i++)
		{
			Node n = e.getChildNodes().item(i);
			if(n.getNodeType() == Node.ELEMENT_NODE && n.getNodeName().equals(tag))
				list.add((Element)n);
		}
		
		return list;
	}
	private static int getTimeout(Element e, int defaultTimeout)
	{
		int timeout = defaultTimeout;
		String timeoutSt = e.getAttribute("timeout");
		if(timeoutSt != null && timeoutSt.length() > 0)
		{
			try{
				timeout = Integer.parseInt(timeoutSt.trim());
			}catch(Exception err){}
		}
		
		return timeout;
	}
	private static String getText(Element e)
	{
		Node first = e.getFirstChild();
		if(first == null || !(first.getNodeType() == Node.TEXT_NODE))
			return null;
		else
			return ((Text)first).getNodeValue();
	}
	
	//executes a set of commands, allowing all to start but not moving on til all are finished
	private static class CommandSet extends Thread implements CommandElement
	{
		private List<CommandElement> commands;
		private boolean finished = false;
		private boolean quit = false;
		private boolean exitLoop = false;
		
		public CommandSet(List<CommandElement> commands)
		{
			this.commands = commands;
		}
		public void run()
		{
			for(CommandElement elem : this.commands)
			{
				elem.execute(true);
			}
			
			boolean allFinished = false;
			boolean hasQuitter = false;
			boolean hasBreaker = false;
			while(!allFinished)
			{

				hasQuitter = false;
				hasBreaker = false;
				allFinished = true;

				for(CommandElement elem : this.commands)
				{
					if(elem.quit()){
						hasQuitter = true;
						break;
					}
					if(elem.exitLoop())
					{
						hasBreaker = true;
						break;
					}
					if(!elem.finished()){

						allFinished = false;
					}
				}
				if(hasQuitter)
				{
					quit = true;
					break;
				}
				else if(hasBreaker)
				{
					exitLoop = true;
					break;
				}
				
				try{
					Thread.sleep(10);
				}catch(Exception e){}
			}
			
			finished = true;
		}
		
		@Override
		public void execute(boolean newThread) {
			if(newThread && !isAlive())
			{
				try{
				start();
				}catch(Exception e)
				{
					run();
				}
			}
			else
				run();
		}
		@Override
		public boolean finished() {
			return finished;
		}
		
		public boolean quit()
		{
			return quit;
		}
		@Override
		public boolean exitLoop() {
			return exitLoop;
		}
	}
	private static class CommandStep extends Thread implements CommandElement
	{
		private int timeout = Integer.MAX_VALUE;
		private String command = "";
		private boolean finished = false;
		private boolean quit = false;
		private boolean quitOnError = false;
		public CommandStep(String command, int timeout, boolean quitOnError)
		{
			this.timeout = timeout;
			this.command = command;
			this.quitOnError = quitOnError;
		}
		
		public void run() {
			boolean b = imp.executeCommand(command, timeout);
			if(quitOnError)
				quit = !b;
			finished = true;
		}
		public void execute(boolean newThread)
		{
			if(newThread && !isAlive())
			{
				try{
				start();
				}catch(Exception e)
				{
					run();
				}
			}
			else
				run();
		}
		public boolean finished()
		{
			return finished;
		}
		public boolean quit()
		{
			return quit;
		}
		@Override
		public boolean exitLoop() {
			return false;
		}
	}
	//evaluates a sequence of commands waiting for each element to finish
	private static class CommandSequence extends Thread implements CommandElement
	{
		private List<CommandElement> commands;
		private boolean finished = false;
		private boolean quit = false;
		private boolean exitLoop = false;
		public CommandSequence(List<CommandElement> steps)
		{
			this.commands  = steps;
		}

		public void run()
		{
			for(CommandElement c : commands)
			{
				c.execute(false);
				if(c.quit()){
					quit = true;
					break;
				}
				else if(c.exitLoop())
				{
					exitLoop = true;
					break;
				}
			}
			finished = true;
		}
		public void execute(boolean newThread)
		{
			if(newThread && !isAlive())
			{
				try{
				start();
				}catch(Exception e)
				{
					run();
				}
			}
			else
				run();
		}
		public boolean finished()
		{
			return finished;
		}
		public boolean quit()
		{
			return quit;
		}
		@Override
		public boolean exitLoop() {
			return exitLoop;
		}
	}
	private static class CommandPrompt extends Thread implements CommandElement
	{
		private String prompt = "";
		private HashMap<String, CommandElement> options;
		private boolean finished = false;
		private boolean quit = false;
		private boolean exitLoop = false;
		
		public CommandPrompt(String prompt, HashMap<String, CommandElement> possibilities)
		{
			this.prompt = prompt;
			this.options = possibilities;
		}
		@Override
		public void run() {
			String result = imp.prompt(prompt, options.keySet());
			CommandElement elem = options.get(result);
			if(elem != null)
			{
				elem.execute(false);
				if(elem.quit())
					quit = true;
				else if(elem.exitLoop())
					exitLoop = true;
			}
			else
			{
				quit = true;
			}
			finished = true;
		}
		@Override
		public void execute(boolean arg0) {
			if(arg0 && !isAlive())
			{
				try{
				start();
				}catch(Exception e)
				{
					run();
				}
			}
			else
				run();
		}
		@Override
		public boolean quit() {
			return quit;
		}
		@Override
		public boolean finished() {
			return finished;
		}
		@Override
		public boolean exitLoop() {
			return exitLoop;
		}
	}
	 private static class CommandWait implements CommandElement
	 {
		 private long startTime = Long.MAX_VALUE;
		 private int millis = 0;
		 public CommandWait(int millis)
		 {
			 this.millis = millis;
		 }
		 @Override
		public void execute(boolean concurrent) {
			startTime = System.currentTimeMillis();
		}
		 @Override
		public boolean finished() {
			return System.currentTimeMillis() - startTime >= millis;
		}
		 public boolean quit()
		 {
			 return false;
		 }
		 @Override
		public boolean exitLoop() {
			return false;
		}
	 }
	 private static class CommandFor extends Thread implements CommandElement
	 {
		 private CommandElement inner;
		 private boolean quit = false;
		 private boolean finished = false;
		 private int cnt = 1;
		 public CommandFor(int cnt, CommandElement inner)
		 {
			 this.cnt = cnt;
			 this.inner = inner;
		 }
		 @Override
		public void run() {
			 forLoopCounter++;
			for(int i = 0; i < cnt; i++)
			{
				inner.execute(false);
				if(inner.quit())
				{
					quit = true;
					break;
				}
				else if(inner.exitLoop())
				{
					break;
				}
			}
			forLoopCounter--;
			finished = true;
		}
		 @Override
		public void execute(boolean arg0) {
			 
			if(arg0 && !isAlive())
			{
				try{
				start();
				}catch(Exception e)
				{
					run();
				}
			}
			else
				run();
			
		}
		 @Override
		public boolean finished() {
			return finished;
		}
		@Override
		public boolean quit() {
			return quit;
		}
		@Override
		public boolean exitLoop() {
			return false;
		}
	 }
	 
	private static class CommandBreak implements CommandElement
	{
		@Override
		public void execute(boolean arg0) {
			return;
		}
		@Override
		public boolean quit() {
			return false;
		}
		@Override
		public boolean exitLoop() {
			return forLoopCounter > 0;
		} 
		@Override
		public boolean finished() {
			return true;
		}
	}
	private static interface CommandElement
	{
		public void execute(boolean newThread);
		public boolean finished();
		public boolean quit();
		public boolean exitLoop();
	}
}
